#!/bin/bash
#   Copyright 2017 bin jin
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.

# Framework:
#
#     If the function names conform to the specifications:
#         External call function.
#         Error handling.
#         Display help information.
#         Print the functions list.
#
#     e.g.
#         ### [brief_introduction] #[description_1] #[description_1]
#         [script_name]_[function_name](){
#             ...
#             [function_body]
#             '''
#             # exit and display [error_description]
#             exit 1; # [error_description]
#             '''
#             # return false status
#             return 1
#         }

### Output version and exit.
_lib_version () {
    printf "0.18.9\n" >&2;
    return 0
}

### Cursor # ##Usage: cur [option] ##    --hide, -    hidden cursor#    --show, +    show cursor ##
_lib_cur () {
    case $1 in
        --hide|-)
            printf '\033[?25l'
        ;;
        --show|+)
            printf '\033[?25h'
        ;;
        "")
            local p=;
            printf '\e[6n';read -sdR p;
            printf "${p#*[}\n"
        ;;
        *)
            exit 1; # invalid option
        ;;
    esac
}

### Create/Convert package file ##Usage: pkg [options] [path] ##    --dmg,     -n  [folder]          Create DMG #    --verify,  -t  [dmg_file]        verify dmg #    --dmg2iso, -di [dmg_file_path]   Convert a DMG file to ISO #    --iso2dmg, -id [iso_file_path]   Convert an ISO file to DMG format #    --udf,     -u  [dir_path]        Create ISO by directory#    --dd,      -w  [img_path] [disk_path] #                                     Write img to disk #    --get,     -g  [[countdown_min]] Get apk file from AppStore #                                  1. Login AppStore and Install App #                                  2. Run this command before download complete #                                  3. Keep shell open ##
_lib_pkg () {
    [[ $OSTYPE == darwin* ]] || exit 1; # Only support macOS (OS X)
    local tmp;
    case $1 in
        --dmg|-n)
            [ -d "$2" ] || exit 1; # target folder not found.
            # trim, # linux:${2:0:-1} -> darwin:${2:0:${#2}-1}
            [ "${2:0-1}" == "/" ] && tmp="${2:0:${#2}-1}" || tmp="$2";
            tmp=${tmp##*/};
            hdiutil create -srcfolder "$2" -skipunreadable -format UDZO "$tmp.dmg" || return 1
        ;;
        --verify|-t)
            [ "${2##*.}" == "dmg" -o "${2##*.}" == "DMG" ] || exit 1; # format error
            hdiutil verify "$2"
        ;;
        --dmg2iso|-di)
            [ -f "$2" ] || exit 1; # dmg file not found
            [ "${2##*.}" == "dmg" -o "${2##*.}" == "DMG" ] || exit 1; # file not dmg
            tmp=${2##*/};
            tmp=${tmp%.*};
            hdiutil convert "$2" -format UDTO -o "$tmp.iso";
        ;;
        --iso2dmg|-id)
            [ -f "$2" ] || exit 1; # iso file not found
            [ "${2##*.}" == "iso" -o "${2##*.}" == "ISO" ] || exit 1; # file not iso
            tmp=${2##*/};
            tmp=${tmp%.*};
            hdiutil convert "$2" -format UDRW -o "$tmp.dmg";
        ;;
        --udf|-u)
            [[ $OSTYPE == darwin* ]] || exit 1; # Only support macOS (OS X)
            [ -d "$2" ] || exit 1; # directory not found
            # hdiutil makehybrid -iso -joliet -o win8.iso win8.cdr
            hdiutil makehybrid -udf -udf-version 1.02 -o "$2.iso" "$2";
            return 0
        ;;
        --dd|-w)
            shift;
                # caller 0;
            [ -f "$1" ] || exit 1; # img file not found
            local target i j k l;
            [ "${2:0:9}" == "/dev/disk" ] && {
                target=$2
            } || {
                # cut mount info
                while read i j k; do
                    [ "${k%% (*}" == "$2" ] && target=$i
                done < <(mount)
            }
            # trim
            [[ $target == *disk[0-9]s[0-9] ]] && target=${target%s*};

            case ${1##*.} in
                img)
                    diskutil unmountDisk $target >/dev/null || exit 1; # Can not unmount this disk
                    sudo dd if="$1" of=$target bs=4m || exit 1; # dd error
                ;;
                iso)
                    diskutil unmountDisk $target >/dev/null || exit 1; # Can not unmount this disk
                    hdiutil convert "$1" -format UDRW -o "${1%.*}.dmg" >/dev/null;
                    sudo dd if="${1%.*}.dmg" of=$target bs=4m || exit 1; # dd error
                    rm "${1%.*}.dmg";
                    # sudo dd if="${1%.*}.dmg" of=$target bs=4m & while killall -USR1 dd; do
                    #     sleep 10;
                    # done | _lib_prefix "%F %T, "
                ;;
                gz)
                    which gzcat >/dev/null 2>&1 || exit 1; # gzcat command not found
                    diskutil unmountDisk $target >/dev/null || exit 1; # Can not unmount this disk
                    gzcat "$1" | sudo dd of=$target bs=4m || exit 1; # dd error
                ;;
                xz)
                    which xzcat >/dev/null 2>&1 || exit 1; # xzcat command not found. try: brew install xz
                    diskutil unmountDisk $target >/dev/null || exit 1; # Can not unmount this disk
                    xzcat "$1" | sudo dd of=$target bs=4m || exit 1; # dd error
                ;;
                *)
                    exit 1; # format not support
                ;;
            esac

            sync;
            diskutil eject $target >/dev/null
            return 0
        ;;
        --get|-g)
            __get_pkg $2
        ;;
        *)
            exit 1; # invalid option
        ;;
    esac
}

# Get apk file from AppStore
__get_pkg () {
    [[ $OSTYPE == darwin* ]] || exit 1; # Only support macOS (OS X)
    # sudo chown -R $USER:`id -gn $USER` /private/var/folders
    # sudo chmod -R 755 /private/var/folders
    [ `ps -ef | grep -c "${0##*/}.*pkg.*-g"` -gt 3 ] && exit 1; # gpkg command is already running

    # Override pkg map: pkg[$serial_number]="$path"
    function ___get_pkg_serial_number () {
        local k v;
        while read k v; do
            # Put in map
            [ "$v" ] && pkg[$k]="$v";
        done < <(find "$1" -type f -iname "*.pkg" -exec ls -i {} \; 2>/dev/null); # *.tmp *.pkg
        return 0
    };

    # hash and move to ~/Downloads
    function ___get_pkg_complete () {
        [ -f "$1" ] || return 1;
        local name=${1##*/};
        name=${name#*_};
        name=${name%.*};

        {
            printf "NAME: ${1##*/}\n";
            printf "SIZE: ";
            du -h "$1" | awk '{print $1}'
            printf "TIME: ";
            _lib_str --dhms $(($(date +%s) - $sec_on));
            _lib_hash "$1"
            mv "$1" ~/Downloads 2>/dev/null || {
                cp "$1" ~/Downloads && printf "\033[31m$1 still exist\033[0m\n"
            }
        } | awk -v name="${name:0:8}, " '{print name $0; fflush(stdout)}'

    };

    local i anchor count_cache=0 pkg=() tmp=/private/var/tmp countdown=$((${1-1} * 60)) sec_on=$(date +%s);

    {

        printf "Will listen \033[4m$countdown\033[0m sec.\n";

        # Reg hard link from /private/var/tmp
        ___get_pkg_serial_number $tmp;

        # Infinite loop
        while :; do

            # Exit if all download complete
            [ $countdown -lt 0 -a ${#pkg[@]} == 0 ] && {
                printf "Exit.\n";
                break
            };
            [ $countdown == 0 ] && printf "Listening \033[4mstop\033[0m.\n";

            # Search download pkg
            [ $((countdown -= 2)) -ge 0 ] && ___get_pkg_serial_number /private/var/folders;

            # foreach pkg array
            [ ${#pkg[@]} -gt 0 ] && for i in ${!pkg[@]}; do
                # Alleady complete
                [ "${pkg[$i]%/*}" == "$tmp" ] && {
                    ___get_pkg_complete "${pkg[$i]}" &
                    unset pkg[$i];
                    continue
                };

                anchor=$tmp/${i}_${pkg[$i]##*/}; # Anchor file path

                # Add hard link
                [ -f "$anchor" ] || {
                    ln "${pkg[$i]}" "$anchor" && printf "Add \033[4m${pkg[i]##*/}\033[0m.\n";
                };

                # Download complete
                [ -f "${pkg[$i]}" ] || {
                    unset pkg[$i];
                    ___get_pkg_complete "$anchor" &
                };
            done;

            [ ${count_cache} != ${#pkg[@]} ] && {
                printf "Count: ${#pkg[@]}\n";
                count_cache=${#pkg[@]}
            };

            sleep 1;
        done;

    } | _lib_prefix "%F %T, GPKG, "

    # wait all thread over
    wait
}

### Remote tools ##Usage: remote [option] [args...] ##    -rsa [[user@]host] [[alias]]  Copy rsa public key to remote #    -tar [option] [[user@host:]target_path] [[user@host:]source_path] #                                  remote tar command #    -scp [pass_word] [command...] Auto scp with password ##
_lib_remote () {
    case $1 in
        -rsa)
            shift;
            __remote_rsa "$@"
        ;;
        -scp)
            shift;
            __remote_auto_scp "$@"
        ;;
        -tar)
            shift;
            __remote_tar "$@"
        ;;
        *)
            exit 1; # invalid option
        ;;
    esac
}

# for _lib_remote
__remote_rsa () {
     [ "$1" ] || exit 1; # first args is empty
    local rsa_pub;
    [ -s ~/.ssh/id_rsa$2.pub ] || ssh-keygen -t rsa -P "" -f ~/.ssh/id_rsa$2;
    printf %s 'mkdir ~/.ssh 2>/dev/null;
printf %s "'`cat ~/.ssh/id_rsa$2.pub 2>/dev/null`'" >> ~/.ssh/authorized_keys;
chmod 600 ~/.ssh/authorized_keys;
chmod 700 ~/.ssh
' | ssh $1 bash || exit 1; # Unknown Error

    local host=${1#*@} user;
    [ "$host" == "$1" ] && user=${1%@*} || user=$USER;

    printf %s "Host    $host
    HostName        $host
    Port            22
    User            $user
    IdentityFile    ~/.ssh/id_rsa$2
"
    return 0
}

# for _lib_remote
__remote_auto_scp () {
    local password;
    password=$1;
    shift;
    expect <<-TCL
	set timeout 150
	spawn scp $*
	expect {
		timeout { exit 2 }
		"*yes/no" { send "yes\r"; exp_continue }
		"*assword:" { send "$password\r" }
	}
	expect {
		timeout { exit 2 }
		"*denied*" { exit 3 }
		"No such" { exit 4 }

####        "100%" { exit 0 }

    }
	expect eof
	TCL

    case $? in
        1)
            exit 1; # Expect Error
        ;;
        2)
            exit 1; # Time Out
        ;;
        3)
            exit 1; # Password Error
        ;;
        4)
            exit 1; # File not found
        ;;
        *)
            exit 1; # Unknown Error
        ;;
    esac
}

__remote_tar () {
    [ "$1" ] || exit 1; # target url is empty
    [ "$2" ] || exit 1; # source url is empty
    local option="$1" target_host target_dir source_host source_dir;
    shift;

    target_dir="${1#*:}";
    [ "$target_dir" == "$1" ] || target_host=${1%%:*};
    # ${target_dir:(-1)} == ${target_dir:0-1}
    [ "${target_dir:0-1}" == "/" ] && target_dir="${target_dir:0:${#target_dir}-1}"

    source_dir="${2#*:}";
    [ "$source_dir" == "$2" ] || source_host=${2%%:*};
    [ "${source_dir:0-1}" == "/" ] && source_dir="${source_dir:0:${#source_dir}-1}"

    # echo $target_host, $target_dir, $source_host, $source_dir
    # return 0

    function ___tar_c () {
        {
            [ "${option/P/}" == "$option" ] && \
            printf %s 'cd "'"${2%/*}"'" >/dev/null && tar '$option' - "'"${2##*/}"'"' || \
            printf %s 'tar '$option' - "'"$2"'"'
        } | ssh $1 bash
    };

    function ___tar_x () {
        [ "${option/P/}" == "$option" ] && {
            ssh $1 'cd "'"$2"'" >/dev/null && tar '$option' -'
        } || ssh $1 'tar '$option' - '
    };

    function ___tar_cat_i () {
        ssh $1 'cat > "'"$2"'"'
    };

    function ___tar_cat_o () {
        ssh $1 'cat "'"$2"'"'
    };

    case $option in
        *c*)
            if [ "$target_host" -a "$source_host" ]; then
                # both
                ___tar_c $source_host "${source_dir}" | ___tar_cat_i $target_host "$target_dir"
            elif [ "$target_host" ]; then
                # local -> remote
                [ -d "$source_dir" ] || exit 1; # source path not found.
                {
                    [ "${option/P/}" == "$option" ] && {
                        cd "${source_dir%/*}" >/dev/null && tar $option - "${source_dir##*/}"
                        cd "$OLDPWD"
                        :
                    } || tar $option - "${source_dir}"
                } | rcat $target_host "$target_dir";
            else
                # remote -> local
                ___tar_c $source_host "$source_dir" > "$target_dir";
            fi
        ;;
        *x*)
            if [ "$target_host" -a "$source_host" ]; then
                # both
                ___tar_cat_o $source_host "${source_dir}" | ___tar_x $target_host "$target_dir"
            elif [ "$target_host" ]; then
                # local -> remote
                [ -f "$source_dir" ] || exit 1; # source file not found.
                cat "$source_dir" | ___tar_x $target_host "$target_dir"
            else
                # remote -> local
                cd "$target_dir";
                ___tar_cat_o $source_host "$source_dir" | tar $option -;
                cd "$OLDPWD"
            fi
        ;;
        *)
            exit 1; # invalid option
        ;;
    esac

    return 0
}

### OS globle setting ##Usage: oset [option] [args...] ##    --allow,     -a          Allow all app install #    --hidden,    - [yes|no]  Hidden all privite file or directory #    --re-launch, -rl         Reset LaunchPad #     --set-launch, -sl [rows] [cols]       setting LaunchPad columes rows#    --uuid,      -u          Get uuid by username. default: [current]#    --sid,       -s          Get sid by username. default: [current]##
_lib_oset () {
    [[ $OSTYPE == darwin* ]] || exit 1; # Only support macOS (OS X)
    case $1 in
        --allow|-a)
            sudo spctl --master-disable;
        ;;
        --hidden|-)
            [ "$2" == "yes" -o "$2" == "no" ] || exit 1; # must select 'yes' or 'no'
            defaults write com.apple.finder AppleShowAllFiles $2;
        ;;
        --re-launch|-rl)
            defaults write com.apple.dock ResetLaunchPad -bool true;
            killall Dock
        ;;
        --set-launch|-sl)
            if [ "$3" ]; then
                defaults write com.apple.dock springboard-rows -int $2
                defaults write com.apple.dock springboard-columns -int $3
                killall Dock
            elif [ "$2" ]; then
                exit 1; # columns not set
            else
                defaults write com.apple.dock springboard-rows Default
                defaults write com.apple.dock springboard-columns Default
                killall Dock
            fi
        ;;
        --uuid|-u)
            [ "$2" ] || set "" $USER;
            dsmemberutil getuuid -U $2
        ;;
        --sid|-s)
            [ "$2" ] || set "" $USER;
            dsmemberutil getsid -U $2
        ;;
        --lock-screen|-l)
            # Ctrl + Command + Q
            /System/Library/CoreServices/Menu\ Extras/User.menu/Contents/Resources/CGSession -suspend
        ;;
        --mobile-sync|--ms)
            open ~/Library/Application\ Support/MobileSync/Backup
        ;;
        --logout)
            # killall -u $USER;
            # kill -9 `ps -Axjc | awk /loginwindow/'{print $2}'`;
            # # System Preferences -> Universal Access -> Enable access for assistive devices
            # echo a | sudo tee /var/db/.AccessibilityAPIEnabled
            printf 'tell application "System Events"
                log out
            end tell
            ' | osascript
        ;;
        --notify)
            shift
            if [ "$3" ]; then
                osascript -e 'display notification "'"$1"'" with title "'"$2"'" subtitle "'"$3"'"'
            elif [ "$2" ]; then
                osascript -e 'display notification "'"$1"'" with title "'"$2"'"'
            else
                osascript -e 'display notification "'"$1"'" with title "Hi"'
            fi
        ;;
        --button-wait)
            osascript -e 'display dialog "'"$2"'" buttons "Yes" with icon note'
        ;;
        --button-choose)
            osascript -e 'display dialog "'"$2"'"'
        ;;

        # Screenshots
        # defaults write com.apple.screencapture location $1

        # DiskUtil Debug
        # defaults write com.apple.DiskUtility DUDebugMenuEnabled 1

        # LaunchPad
        # defaults write com.apple.dock ResetLaunchPad -bool true; killall Dock

        # Spotlight close
        # launchctl unload -w /System/Library/LaunchDaemons/com.apple.metadata.mds.plist

        --sleep)
            sudo osascript -e 'tell application "Finder" to sleep'
        ;;
        *)
            exit 1; # invalid option
        ;;
    esac
}

### Print md5,sha1,sha256 checksums ##Usage: hash [file_path] ##
_lib_hash () {
    which openssl >/dev/null 2>&1 || exit 1; # openssl command not found
    local h_code;
    for h_code in $(
        {
            [ -f "$1" ] && {
                cat "$1" | tee >(openssl dgst -sha1 >&2) >(openssl dgst -md5 >&2) >(openssl dgst -sha256 >&2) | openssl dgst -sha512;
                :
            } || tee >(openssl dgst -sha1 >&2) >(openssl dgst -md5 >&2) >(openssl dgst -sha256 >&2) | openssl dgst -sha512;
        } 2>&1
    ); do
        if [ ${#h_code} == 32 ]; then
            printf "MD5: $h_code\n";
        elif [ ${#h_code} == 40 ]; then
            printf "SHA1: $h_code\n";
        elif [ ${#h_code} == 64 ]; then
            printf "SHA256: $h_code\n";
        elif [ ${#h_code} == 128 ]; then
            printf "SHA512: $h_code\n";
        fi
    done
}

### Volume info or edit ##    --wipes, -w  [path]   Wipes the free space on the volume #    --createinstallmedia, -im [device_path] #                          Create instatll media, by Install*.app
_lib_vol () {
    case $1 in
        # --encrypts-all|-eca)
        #     local args;
        #     while read args; do
        #         set $args;
        #         [ "${2#/Volumes/}" == "$2" ] || diskutil apfs enableFileVault $1 -user disk
        #     done < <(__vol_list)
        # ;;
        # --encrypts|-ec)
        #     __vol_list
        # ;;
        --wipes|-w)
            [ -d "$2" ] || exit 1; # Target path not found
            diskutil secureErase freespace 3 $2
        ;;
        --createinstallmedia|-im)
            [ -d "$2" ] || exit 1; # target device not found
            [ -d "$3" ] || set "" "$2" "/Applications/Install";
            while read tmp; do
                break;
            done < <(find "$3"* -iname "createinstallmedia" 2>/dev/null);
            [ -f "$tmp" ] || exit 1; # install source not found
            [ -f "${tmp%Resources*}SharedSupport/InstallInfo.plist" ] || {
                if [ -f "/macOS Install Data/InstallInfo.plist" ]; then
                    sudo mkdir -p "${tmp%Resources*}SharedSupport";
                    find "/macOS Install Data" -type f -exec sudo ln {} "${tmp%Resources*}SharedSupport" \;
                else
                    exit 1; # need to run the 'install app' and exit before 'reboot'
                fi
            };
            sudo "$tmp" --volume "$2" --applicationpath "${tmp%/Contents*}" --nointeraction;
        ;;
        *)
            exit 1; # invalid option
        ;;
    esac
}

# __vol_list () {
#     local args;
#     while read args; do
#         set ${args/+->/#} >/dev/null 2>&1 || continue;
#         [ "$1" == "|" ] && shift;
#         [ $# -le 1 ] && continue;
#
#         if [ "$1$2" == "#Volume" ]; then
#             printf "$3 $4 "
#         # elif [ "$1$2$3" == "APFSVolumeDisk" ]; then
#         #     printf "$5 "
#         elif [ "$1$2" == "MountPoint:" ]; then
#             if [ "$3" == "Not" ]; then
#                 printf %s "- "
#             else
#                 printf "$3 "
#             fi
#         elif [ "$1" == "FileVault:" ]; then
#             printf "$2\n"
#         fi
#     done < <(diskutil apfs list)
# }

### Directory tools ##Usage: dir [option] [args...] ##    --unhidden, -uh [dir_path]   No hidden directory #    --own           [dir_path]   Set directory owner #    --boot          [dir_path]   Rebuild boot file #    --count,    -c  [[dir_path]] Get file count #    --unique,   -u  [[dir_path]] Search for duplicate files##
_lib_dir () {
    case $1 in
        --unhidden|-uh)
            shift;
            [[ $OSTYPE == darwin* ]] || exit 1; # Only support macOS (OS X)
            [ -d "$1" ] || exit 1; # target not found or not directory
            chflags nohidden "$1"/*;
            sudo spctl --master-disable;
        ;;
        --own)
            shift;
            [ -d "$1" ] || exit 1; # directory not found
            local USER arg;
            USER=${2:-bin};
            # ${1:(-1)} == ${1:0-1}
            [ ${1:(-1)} == / ] && arg="${1:0:${#1}-1}" || arg="$1";
            sudo chown -R $USER:`id -gn $USER` "$arg" || return 1;
            sudo chmod -R 644 "$arg/*"
            sudo find "$arg" -type d -exec chmod 755 {} \;
        ;;
        --boot)
            [[ $OSTYPE == darwin* ]] || exit 1; # Only support macOS (OS X)
            [ -f "$2/com.apple.recovery.boot/kernelcache" ] || exit 1; # Boot file not found
            sudo bless --folder "$2/com.apple.recovery.boot" --label "Recovery HD";
            # 426F6F74-0000-11AA-AA11-00306543ECAC
            # [ "${1##*/}" == "dmtest" -a "${2##*/}" == "BaseSystem.dmg" -a -d "$3" ] && "$2" ensureRecoveryPartition "$3" "$2" 0 0 "${2%.*}.chunklist"
        ;;
        --count|-c)
            [ "$2" ] && {
                [ -d "$2" ] || exit 1; # target not found or not directory
                :
            } || set "" \.;
            # ls -alR "$2" 2>/dev/null | grep -c '^-'
            find "$2" -type f | grep -c '\/'
        ;;
        --unique|-u)
            [[ $OSTYPE == darwin* ]] && exit 1; # Not support macOS (OS X)
            shift;
            # TODO
            local i pi size path1 sum name1 temp tmp path_arr=() name_arr=() out=".#Trash";
            touch "$out.log" || exit 1; # output error
            mkdir -p "$out";
            while read size path1; do
                [ "$temp" == "$size" ] || {
                    [ ${#path_arr[@]} -gt 1 ] && \
                    while read sum name1; do
                        [ "$sum" ] && printf "${sum:0:8},${name1//%/%%}\n";
                        [ "$tmp" == "$sum" ] || {
                            [ ${#name_arr[@]} -gt 1 ] && {

                                # save first file
                                unset name_arr[0];
                                for pi in ${!name_arr[@]}; do
                                    pi="${name_arr[$pi]}";
                                    printf "$temp,MOVE ";
                                    # move file to new file
                                    if [ "$pi" == "${pi%/*}" ]; then
                                        mv -v "./$pi" "$out"
                                    else
                                        mkdir -p "$out/${pi%/*}";
                                        mv -v "./$pi" "$out/${pi%/*}"
                                    fi
                                done

                                printf "\n"
                            };

                            # sync status
                            tmp="$sum";
                            name_arr=()
                        };
                        [ -f "$name1" ] && name_arr[${#name_arr[@]}]="$name1"
                    done < <(
                        {
                            for i in ${!path_arr[@]}; do
                                sha256sum "${path_arr[$i]}" &
                            done
                            wait
                        } | sort --key=1 | uniq --all-repeated --check-chars=64;
                        printf "\n";
                    );

                    # sync status
                    temp="$size";
                    path_arr=()
                };
                [ -f "$path1" ] && path_arr[${#path_arr[@]}]="${path1#*./}";
            done < <(
                find "${@:-.}" -not -empty -type f -not -iregex ".*$out.*" -printf "%s            %p\n" | \
                    sort --key=1 | \
                    uniq --all-repeated --check-chars=12;
                printf "\n";
            ) 2>&1 | tee -a $out.log;

        ;;
        *)
            exit 1; # invalid option
        ;;
    esac
}

### String manage ##Usage: str [option] ...##    --random,   -r   [count]      Print random string #    --lower,    -l   [str]        Convert lower case #    --upper,    -r   [str]        Convert upper case #    --tac            [[file]]#    --xmlvalue, -xv  [tag_name]   Get xml value #    --unascii,  -ua  [dir_path]   Print text not in ASCII char #    --dhms,     -t   [secends]    Format Unix timestamp to ??d ??h ??m ??s #    --passwd,   -pw  [passwd_str] Get password for stdin chpasswd -e ##
_lib_str () {
    case $1 in
        --lower|-l)
            shift;
            [ "$1" ] && {
                tr "[:upper:]" "[:lower:]" <<< "$@"
            } || tr "[:upper:]" "[:lower:]";
        ;;
        --upper|-u)
            shift;
            [ "$1" ] && {
                tr "[:lower:]" "[:upper:]" <<< "$@";
            } || tr "[:lower:]" "[:upper:]"
        ;;
        --xmlvalue|-xv)
            [ "$2" ] || exit 1; # Args is empty
            # grep -oP '(?<=\>)[\d\w_]+(?=\<\/'$2')' && return 0;
            perl -ne 'STDOUT->autoflush; print m/(?<=\>)[\d\w_]+(?=\<\/'"$2"')/g, "\n"' && return 0;
            return 1
        ;;
        --tac)
            if which perl >/dev/null; then
                [ "$2" ] && {
                    perl -e 'print reverse <>' "$2"
                    :
                } || perl -e 'print reverse <>'
            else
                [ "$2" ] && {
                    sed '1!G;h;$!d' "$2"
                    :
                } || sed '1!G;h;$!d'
            fi
        ;;
        # --xmltrim|-xt)
        #     # -xt [text_file_path] Get xml without annotations
        #     shift;
        #     awk '/<!--/{i++}/-->/{i--}!i&&!/-->/&&!/^$/' "$@"
        # ;;
        # --spacetrim|-st)
        #     -st [text_file_path]  Replace all space to one space
        #     [ -f "$2" ] || exit 1; # File not found
        #     [ "`uname`" == "Linux" ] && {
        #         sed -r ':1;N;$!b1;s/\s+/ /g' "$2"
        #         :
        #     } || perl -i -pe 's/\s+/ /g' "$2"
        # ;;
        --unascii|-ua)
            if [ -d "$2" ]; then
                grep -r '[^[:alnum:][:punct:][:space:]]' "$2";
            elif [ -f "$2" ]; then
                grep '[^[:alnum:][:punct:][:space:]]' "$2";
            else
                grep '[^[:alnum:][:punct:][:space:]]'
            fi
        ;;
        --now|-n)
            date +%Y%m%d%H%M%S
        ;;
        --random|-r)
            # local rand=`dd if=/dev/random bs=1 count=${1:-32} 2>/dev/null | base64 | tr -d '\n'`; # tr -d '\n' support linux
            openssl rand -base64 ${2:-9} | tr -d '\n' | sed "s/[^0-9A-Za-z]/${RANDOM:0:1}/g"
        ;;
        --uuid|-u)
            [[ $OSTYPE == darwin* ]] && uuidgen
        ;;
        --dhms|-t)
            _lib_inum $2 || exit 1; # Not number
            local out=$(($2 % 60))s;
            [ $2 -ge 60 ] && {
                out=$(($2 % 3600 / 60))m\ $out;
                [ $2 -ge 3600 ] && {
                    out=$(($2 % 86400 / 3600))h\ $out;
                    [ $2 -ge 86400 ] && out=$(($2 / 86400))d\ $out
                }
            };
            printf "$out\n"
        ;;
        --passwd|-pw)
            which openssl >/dev/null 2>&1 || exit 1; # openssl command not found
            [ "$2" ] || exit 1; # Args is empty
            openssl passwd -1 "$2"
        ;;
        *)
            exit 1; # invalid option
        ;;
    esac
}

### Show/find IPv4 ##Usage: ip [option] [args...] ##    --list, -l            Show IPv4 #    --find, -f [mac_addr] Search IPv4 by MAC or Host name, #                          support alias, see lib hosts #    --test, -t [str]      Test string is ipv4 ##
_lib_ip () {
    case $1 in
        --list|-l)
            # netstat -rn | grep "default"
            local route_ip=`route -n get default | awk '/gateway/{print $2}'`;
            ifconfig | awk '/inet '${route_ip%.*}'\.[0-9]+/{print $2}';
        ;;
        --find|-f)
            shift;
            source "$(cd `dirname $0`; pwd)/_lib";
            __ip_find "$@"
        ;;
        --test|-t)
            shift;
            [ "$1" ] || return 1;
            [ ${#1} -gt 15 ] && return 1;
            # command test
            [ "${1:0:1}" == "-" ] && return 1;
            [[ $1 != *[^.0-9]* && $1 == [1-9]*[0-9] && $1 != *..* ]] || return 1;
            local a IFS=$IFS".";
            a=($1);
            [ ${#a[@]} == 4 ] || return 1;
            for a in ${a[@]}; do
                [ $a -gt 255 ] && return 1;
            done
            return 0
        ;;
        *)
            exit 1; # invalid option
        ;;
    esac
}

# for _lib_ip
__ip_find () {
    [ "$1" ] || exit 1; # MAC addr or Host name is empty

    # Clear arp cache
    [[ $OSTYPE == darwin* ]] && {
        sudo arp -ad >/dev/null 2>&1 || return 1;
        :
    } || arp -n | awk 'NR>1 {system("arp -d "$1)}' || return 1;

    local i j max_len=0 r key mac range route_ip;

    # get [setting]
    __load_ini sip_setting;
    route_ip=`_map --get route`;
    route_ip=(${route_ip//,/ } `route -n get default | awk '/gateway/{print $2}'`);
    # trim
    for i in ${!route_ip[@]}; do
        _set --add "${route_ip[$i]%.*}" >/dev/null || unset route_ip[$i];
    done
    range=`_map --get range`;
    [ "$range" ] || range="1-127";
    _map --clear;

    __load_ini hosts;

    local arg mac_addr mac_addr_arr=() tmp;
    for arg in "$@"; do

        # get from map
        mac_addr=`_map --get $arg`;
        mac_addr=${mac_addr:-$arg};

        # replace separator, skip type like: a-bc
        [[ $mac_addr == *-*-*-*-*-* ]] && mac_addr=${mac_addr//-/:};

        [[ $mac_addr == *:*:*:*:*:* ]] && {
            # cache max variable length
            [ ${#arg} -gt $max_len ] && max_len=${#arg};
            # convert lowercase
            mac_addr=`_lib_str --lower $mac_addr`;

            if [[ $OSTYPE == darwin* ]]; then
                # trim ^0, 0a:0b:1c:1d:1e:1f -> a:b:1c:1d:1e:1f
                tmp="";
                for i in ${mac_addr//|/ }; do
                    [[ $i == *:*:*:*:*:* ]] && \
                    for j in {0..9} {a..f}; do
                        i=${i//0$j/$j}
                    done
                    [ "$tmp" ] && tmp="$tmp|$i" || tmp="$i"
                done
                mac_addr=$tmp;
            fi

            # add prefix
            mac_addr_arr[${#mac_addr_arr[@]}]=$arg@$mac_addr;
        }
    done

    [ ${#mac_addr_arr[@]} == 0 ] && return 0;

    # for offset
    max_len=$((max_len / 5 * 5 + 5));

    # fork: Resource temporarily unavailable
    function __match_mac () {
        local i j key;
        ping -c 2 -t 2 $1 >/dev/null 2>&1;
        j=`arp $1 2>/dev/null`;
        for mac_addr in ${mac_addr_arr[@]}; do
            # cache key
            key=${mac_addr%%@*};
            # split arr with '|'
            mac_addr=(${mac_addr//[@|]/ });
            unset mac_addr[0];
            for i in ${mac_addr[@]}; do
                [[ $j == *$i* ]] && printf "%-${max_len}s$1\n" $key
                # Like nbtstat
                # [[ `smbutil status $1 2>/dev/null` == *$i* ]] && printf $1
            done
        done
    };

    # init thread valve
    _thread_valve --init 32;

    for r in ${route_ip[@]}; do
        for i in `seq ${range/-/ }`; do
            # get thread
            _thread_valve --run __match_mac ${r%.*}.$i
        done
    done

    # destroy thread valve
    _thread_valve --destroy;

    # print ipv4 for mac addr not catch
    for mac_addr in ${mac_addr_arr[@]}; do
        [ "${mac_addr/@/}" == "$mac_addr" -o "${mac_addr/[0-9].[0-9]*.*[0-9].[0-9]/}" == "$mac_addr" ] && continue;
        # cache key
        key=${mac_addr%%@*};
        # split arr with '|'
        mac_addr=(${mac_addr//[@|]/ });
        unset mac_addr[0];
        for i in ${mac_addr[@]}; do
            _lib_ip --test $i && printf "%-${max_len}s$i\n" $key
        done
    done

    # clear arp cache
    [[ $OSTYPE == darwin* ]] && {
        sudo arp -ad >/dev/null 2>&1 || return 1;
        :
    } || arp -n | awk 'NR>1 {system("arp -d "$1)}' || return 1;

}

# __hosts () {
#     printf %s '##
# # Host Database
# #
# # localhost is used to configure the loopback interface
# # when the system is booting.  Do not change this entry.
# ##
# 127.0.0.1	localhost
# 255.255.255.255	broadcasthost
# ::1             localhost
# '
# }

### Update hosts by ini #    will read ini config from HOME or script path. #    ini file context like: # #[hosts] #abc.org=01-12-23-34-45-56 #lib-233=67-78-89-9a-ab-bc #www.a.com=192.168.1.1 ##
_lib_hosts () {
    source "$(cd `dirname $0`; pwd)/_lib";

    # load ini in map1
    __load_ini hosts 1 || exit 1; # Not found ini configure

    # get map1 key array in MAP2ARRAY
    _map 1 --keys;
    local i j k line host_new lf;
    printf -v lf "\n";

    while read line; do
        # override mac addr in map1, if value not ipv4
        _lib_ip --test "$(_map 1 --get ${line%% *})" || {
            _map 1 --put $line >/dev/null;
            printf "%s\n" "$line"
        }
    done < <(_lib_ip --find ${MAP2ARRAY[@]});

    # clear map0, sip command use it before
    _map --clear;

    # get map1 key value in MAP2ARRAY
    _map 1 --arr || exit 1; # Unknown error

    for ((i=0; i<${#MAP2ARRAY[@]}; i+=2)); do
        _lib_ip --test ${MAP2ARRAY[i + 1]} && _map --put "${MAP2ARRAY[$i]}" "${MAP2ARRAY[i + 1]}" >/dev/null;
    done

    while read line; do
        k=($line);
        j=`_map --get "${k[1]}"` && {
            _map --remove "${k[1]}" >/dev/null;
            [ "$j" == "$k" ] || {
                host_new+="${line/$k/$j}$lf";
                continue
            };
        }
        host_new+="$line$lf";
    done < <(cat /etc/hosts);

    # empty array
    _map --arr || unset MAP2ARRAY;

    for ((i=0; i<${#MAP2ARRAY[@]}; i+=2)); do
        host_new+="${MAP2ARRAY[i + 1]}	${MAP2ARRAY[i]}$lf"
    done

    printf %s "$host_new" | sudo tee /etc/hosts >/dev/null

}

# # Test mac addr
# __imac () {
#     [ "$1" ] || return 1;
#     # command test
#     [ "${1:0:1}" == "-" ] && return 1;
#     [[ $1 != *[^-:0-9a-fA-F]* && $1 != *--* && $1 != *::* && $1 == [0-9a-fA-F]*[0-9a-fA-F] && $1 != *[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]* ]] || return 1
#     local a IFS=$IFS":-";
#     a=($1);
#     [ ${#a[@]} == 6 ] || return 1;
#     return 0
# }

### Connect vnc server ##Usage: vnc [ipv4][:[port]] ##server listening port 5900 ##
_lib_vnc () {
    [[ $OSTYPE == darwin* ]] || exit 1; # Only support macOS (OS X)
    _lib_ip --test $1 || exit 1; # Args not a ipv4
    open vnc://$1;
    return 0
}

### Test str is num ##Usage: inum [str] ##
_lib_inum () {
    [ "$1" ] || return 2;
    [[ $1 != *[^0-9]* && $1 != 0[0-9]* ]] || return 1;
    return 0
}

### Tag prefix each line, support `date` format ##Usage: [command] | prefix [strftime format] ##
_lib_prefix () {
    [ "$1" ] || exit 1; # Args is empty
    source "$(cd `dirname $0`; pwd)/_lib";
    _prefix "$@"
}

### Plist editer ##Usage: plist [option] [path...] ##    --xml,    -x [plist_path]   print plist as xml format #    --json,   -j [plist_path]   print plist as xml format #    --binary, -b [plist_path]   serialize plist to stdout #    --load       [plist_path]   load plist #    --unload     [plist_path]   unload plist ##
_lib_plist () {
    [[ $OSTYPE == darwin* ]] || exit 1; # Only support macOS (OS X)
    which plutil >/dev/null 2>&1 || xcode-select --install 2>/dev/null
    [ $# -lt 2 ] && exit 1; # invalid option
    [ "${2##*.}" == "plist" ] || exit 1; # file format not support
    case $1 in
        --xml|-x)
            [ -f "$2" ] || exit 1; # file not exist
            plutil -convert xml1 "$2" -o - || exit 1; # plutil error
        ;;
        --json|-j)
            [ -f "$2" ] || exit 1; # file not exist
            plutil -convert json -r "$2" -o - || exit 1; # plutil error
            printf "\n"
        ;;
        --binary|-b)
            [ -f "$2" ] && exit 1; # file aleady exist
            sudo plutil -convert binary1 -o "$2" - || exit 1; # plutil error
        ;;
        --load)
            [ -f "$2" ] || exit 1; # file not exist
            sudo launchctl load -wF "$2"
        ;;
        --unload)
            [ -f "$2" ] || exit 1; # file not exist
            sudo launchctl unload -w "$2"
        ;;
        *)
            exit 1; # invalid option
        ;;
    esac
}

# __file_sub_time () {
#     [ -f "$1" ] || return 1;
#     printf $(( (`date +%s` - `stat -f "%a" "$1"`) / 60 / 60 ))
# }

# load .*.ini config
__load_ini () {
    [ "$1" ] || return 1;
    local line is_catch=false;
    while read line; do
        [ "$line" == "" ] && continue;
        if [ "${line/=/}" == "$line" ]; then
            [ "$line" == "[$1]" ] && is_catch=true || is_catch=false;
        elif $is_catch; then
            _map $2 --put ${line%%=*} ${line#*=} >/dev/null
        fi
    done < <(cat ${0%/*}/.*.ini ~/.*.ini 2>/dev/null | awk -F \; 'gsub(/\r|\\$/, ""); gsub(/\\/, "\\\\"); {print $1}' | uniq);

    # # read environment variable
    # while read line; do
    #     [ "$line" == "" -o "${line/=/}" == "$line" ] && continue;
    #     _lib_ip --test ${line#*=} || {
    #         __imac ${line#*=} || continue
    #     };
    #     _map $2 --put "${line%%=*}" "${line#*=}" >/dev/null;
    # done < <(set);

   [ `_map $2 --size` == 0 ] && return 1;
   return 0
}

# # args path size(MB)
# __ramfs () {
#     # https://gist.github.com/Roman2K/3238fb441e298369198e
#     case $1 in
#         --mount|-m)
#             # hdid = hdiutil attach
#             local ramfs_dev=`hdid -nomount ram://$(($3 * 1024 * 1024 / 512))`;
#             newfs_hfs -v "ramfs" ${ramfs_dev};
#             mkdir -p "$2";
#             # mount -o nobrowse -t hfs ${ramfs_dev} "$2"
#             mount -o noatime -t hfs ${ramfs_dev} "$2"
#         ;;
#         --umount|-u)
#             ramfs_dev=`df "$2" | tail -1 | awk '{ print $1 }'`;
#             umount "$2";
#             hdiutil detach ${ramfs_dev}
#         ;;
#         *)
#             printf "\033[31m$1: invalid option\033[0m\n" >&2
#         ;;
#     esac
# }

#################################################
#                   Framework                   #
# # # # # # # # # # # # # # # # # # # # # # # # #

# Print Error info
_func_err () {
    [[ "$4$6" == exit_${0##*/}* ]] && {
        # local err=`awk 'NR=='$2'{print}' "$0"`;
        local err=`sed -n $2p "$0"`
        # Print line text after '#'
        printf "\033[31mError:${err##*#} \033[33m($0:$2)\033[0m\n" >&2;
        exit $(($5 % 256))
    };

    # WARRAN: 0 <= $? <= 255, return 256: $? = 0
    [ "$4" == "return" ] && exit $(($5 % 256));

    # Get script line
    [ $1 == 127 ] && {
        # No function found
        printf "\033[31mError: No function found \033[0m\n" >&2;
        exit 1
    };

    exit 0
}

# Show function info
_func_annotation () {
    local i j k OLDIFS IFS=$IFS\({;
    # Cache IFS
    OLDIFS=$IFS;

    [ "$1" ] && {
        # show select
        while read i j; do
            # Make array splite with #
            [ "$i" == "###" ] && {
                IFS=#;
                k=($j);
                # Reset IFS
                IFS=$OLDIFS
            };
            # At target func name
            [ "$k" -a "$i" == "_${0##*/}_$1" ] && {
                # Print all annotation
                for i in ${!k[@]}; do
                    printf "${k[$i]}\n";
                done;
                return 0
            };
            # Reset var
            [[ "$i" == _${0##*/}* ]] && [ "$j" == ")" ] && unset k;
        done < "$0"; # Scan this script
        return 1
    } || {
        # show all
        while read i j; do
            # Cache intro
            [ "$i" == "###" ] && k=${j%%#*};
            # At func name
            [ "${i%_*}" == "_${0##*/}" -a "$j" == ")" ] && {
                # Left aligned at 15 char
                printf "%-15s$k\n" ${i##*_};
                # Clear var
                unset k
            };
        done < "$0"; # Scan this script
    }

}

# Cache exit
trap '_func_err $? $LINENO $BASH_LINENO $BASH_COMMAND ${FUNCNAME[@]}' EXIT

# # # # # # # # # # # # # # # # # # # # # # # # #
#                   Framework                   #
#################################################

# Test if help
[[ ! "$1" || "$1" == "-h" || "$1" == "--help" ]] && {
    _func_annotation | sort;
    exit 0
} || [[ "$2" == "-h" || "$2" == "--help" ]] && {
    # Test if help
    _func_annotation $1 || printf "\033[31mError: No function found \033[0m\n" >&2;
    exit $?
};

# $*, "$*", $@ can not keep $#, only "$@" can save native structural

# main
_${0##*/}_"$@"

# hdiutil attach *.dmg
# hdiutil detach *.dmg
# sudo bless --folder=/Volumes/BOOT --file=/Volumes/BOOT/EFI/BOOT/BOOTX64.efi --setBoot --lgacy
# sudo asr restore -source /Volumes/OS\ X\ Install\ ESD/BaseSystem.dmg -target /Volumes/Untitled -erase -format HFS+

# diskutil rename "$1" "RHD"
# touch .metadata_never_index
# mkdir com.apple.recovery.boot
#    BaseSystem.chunklist
#    boot.efi
#    BaseSystem.dmg
#    com.apple.Boot.plist
#    .disk_label
#    PlatformSupport.plist
#    kernelcache
#    .disk_label_2x
#    SystemVersion.plist


# Erasing Disk: 0%... 10%... 20%... 30%...100%...
# Copying installer files to disk...
# Copy complete.
# Making disk bootable...
# Copying boot files...
# Copy complete.
# Done.

    # .Trashes
    # .fseventsd
    # .IABootFiles
    # Install OS X Yosemite.app
    # .IABootFilesSystemVersion.plist
    # Library
    # .IAPhysicalMedia
    # System
    # .Spotlight-V100
    # usr

# ./System/Library/CoreServices/boot.efi
# ./usr/standalone/i386/boot.efi
